/**
 * confss is the Configuration Manager of the CSS responsible for distributing the dynamic configurations
 * of subsystem and keeping the registration of all services
 */
package za.org.jcicada.confss.impl;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang3.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.cfgmngr.IConfigurationClient;
import za.org.jcicada.cfgmngr.IConfigurationServer;
import za.org.jcicada.cfgmngr.impl.ConfigurationServerProxy;
import za.org.jcicada.cfgmngr.impl.PropertyLoader;
import za.org.jcicada.genss.ServiceURN;
import za.org.jcicada.genss.SystemURN;
import za.org.jcicada.genss.impl.GenSS;
import za.org.jcicada.genss.messages.Result;
import za.org.jcicada.genss.messages.SuccessCode;
import za.org.jcicada.genss.sensor.impl.Sensor;

/**
 * The Configurable Software Subsystem (confss) offers functionality to
 * automatically load a static configuration from a properties file, 
 * gets its dynamic configuration from CfgMngr, register/deregister itself
 * and its services with CfgMngr as required.
 */
public abstract class ConfSS extends GenSS implements IConfigurationClient {

    /** Class logger */
    private static final Log logger = LogFactory.getLog(ConfSS.class);

    /** Location URI for the Configuration Server */
    protected URI cfgmngrLocationURI;
    
    /** Flag set to indicate that configuration has been received from CfgMngr */
    private boolean configurationSuccess = false;

    /** Collection of services and sensors that are registered */
    protected Set<Pair<ServiceURN, URI>> services = new HashSet<Pair<ServiceURN, URI>>();
    
    /** Object used to monitor cpu usage of this process. */
    private CpuUsage cpuUsage;
    
    /* ************************************************************************
     * Confss configuration classes
     * ************************************************************************/
    
    /** The name of the subsystem properties file */
    public static final String STATIC_PROPERTIES_FILE_PFX = ".static.properties";
    public static final String DYNAMIC_PROPERTIES_FILE_PFX = ".dynamic.properties";
    
    /** The static configuration details */
    protected ConfssStaticConfiguration confssStatic = new ConfssStaticConfiguration();
    
    /** The dynamic configuration details loaded from CfgMngr */
    protected ConfssDynamicConfiguration confssDynamic = new ConfssDynamicConfiguration();

    /** Confss Processor thread doing all periodic registration/deregistration processing */
    protected ConfssProcessing confssProcessing;
    
    /** Handle to ConfigurationServer */
    protected IConfigurationServer configurationServer;
    
    /* ***********************************************************************
     * Start of public API
     *************************************************************************/

    /**
     * Constructor. specifies the subsystem URI
     * @param systemURN the system identifier
     * @param cfgmngrLocationURI the location URI for the Configuration Manager
     */
    public ConfSS(SystemURN systemURN, URI cfgmngrLocationURI) throws URISyntaxException {
        super(systemURN);
        this.cfgmngrLocationURI = cfgmngrLocationURI;
        loadStaticConf();
        confssProcessing = new ConfssProcessing(this);
    }
    
    /* ************************************************************************
     * IConfigurationClient methods
     * ************************************************************************/
    
    /**
     * @see IConfigurationClient#replyGetConfiguration Derived class should
     *      extend confssDynamic and override this method if more dynamic
     *      configuration is required
     */
    public void replyGetConfiguration(Result result, Object configuration) {
        if (loadDynamicConf(configuration)) {
            // Dynamic configuration loaded successful
            cpuUsage = new CpuUsage(confssDynamic);
            // and then start monitoring cpu usage
            cpuUsage.initialize(getTimer(), confssDynamic.cpuPeriod_ms);
            try {
                setupAcceptors(confssDynamic.lifecycleURLs, confssDynamic.sensorsURLs, confssDynamic.alarmsURLs);
                // Processing: stop periodic request for dynamic configuration
                confssProcessing.removeConfigurationRequest(getSystemURN());
                configurationSuccess = true;
                // Processing: Start system registration 
                confssProcessing.addSystemToRegister(getSystemURN());
            } catch (Exception e) {
                String message = String.format("Cannot setup acceptors for URLs received in dynamic configuration for %s (lifecycleURLs:%s, sensorsURLs:%s, alamrsURLs:%s", 
                        getSystemURN().toString(), confssDynamic.lifecycleURLs.toString(), confssDynamic.sensorsURLs.toString(), confssDynamic.alarmsURLs.toString());
                logger.fatal(message);
            }
        } else {
            String message = String.format("Invalid dynamic configuration received for %s", getSystemURN().toString());
            logger.fatal(message);
        }
    }
    
    /**
     * @see IConfigurationClient#replyRegisterSystem
     */
    public void replyRegisterSystem(Result result, SystemURN systemURN, boolean reregister) {
        if (!configurationSuccess) {
            logger.fatal(String.format("System (%s) not configured!! %s - %s", getSystemName(), result.getCode().toString(), result.getMessage()));
        } else if (!result.getCode().equals(SuccessCode.COMPLETE_SUCCESS)) {
            logger.fatal("replyRegisterSystem reported failure " + result.getCode().toString() + " - " + result.getMessage());
        } else {
            // System registration was successful
            // Note Processing: do not stop system registration as this has to be performed periodically continuously
            if (confssProcessing.isServiceProcessingAllowed() == false) {
                // Processing: allow service processing to start
                confssProcessing.allowServiceProcessing(true);
                // Move confss into MANAGE mode
                setMode(ModeEnum.MANAGE);
            }
            if (reregister == true) {
                // Processing: reregister services
            	for (Pair<ServiceURN, URI> entry : services) {
            		confssProcessing.addServiceToRegister(entry);
            	}
            }
        }
    }

    /**
     * @see IConfigurationClient#replyRegisterService
     */
    public void replyRegisterService(Result result, URI locationURI, ServiceURN serviceURN) {   
        if (!configurationSuccess) {
            logger.fatal(String.format("System (%s) not configured!! %s - %s", getSystemName(), result.getCode().toString(), result.getMessage()));
        } else if (result.getCode().equals(SuccessCode.COMPLETE_FAILURE)) {
            logger.error("replyRegisterServices reported failure " + result.getCode().toString() + " - " + result.getMessage());
        } else {
            if (result.getCode().equals(SuccessCode.PARTIAL_SUCCESS)) {
                logger.error("replyRegisterServices reported partial failure " + result.getCode().toString() + " - " + result.getMessage());
            }
            // Processing: stop service registration
            confssProcessing.removeServiceToRegister(new Pair<ServiceURN, URI>(serviceURN, locationURI));
        }
    }

    /**
     * @see IConfigurationClient#replyDeregisterServices
     */
    public void replyDeregisterService(Result result, URI locationURI, ServiceURN serviceURN) {
        if (!configurationSuccess) {
            logger.fatal(String.format("System (%s) not configured!! %s - %s", getSystemName(), result.getCode().toString(), result.getMessage()));
        } else if (result.getCode().equals(SuccessCode.COMPLETE_FAILURE)) {
            logger.fatal("replyDeregisterServices reported failure " + result.getCode().toString() + " - " + result.getMessage());
        } else {
            if (result.getCode().equals(SuccessCode.PARTIAL_SUCCESS)) {
                logger.warn("replyDeegisterServices reported partial failure " + result.getCode().toString() + " - " + result.getMessage());
            }
            //Processing: stop service deregistration (serviceURIs)
            confssProcessing.removeServiceToDeregister(new Pair<ServiceURN, URI>(serviceURN, locationURI));
        }  
    }

    /**
     * @see IConfigurationClient#replyDeregisterSystem
     */
    public void replyDeregisterSystem(Result result, SystemURN systemURN) {
        if (!configurationSuccess) {
            logger.fatal(String.format("System (%s) not configured!! %s - %s", getSystemName(), result.getCode().toString(), result.getMessage()));
        } else if (!result.getCode().equals(SuccessCode.COMPLETE_SUCCESS)) {
            logger.fatal("replyDeregisterSystem reported failure " + result.getCode().toString() + " - " + result.getMessage());
            //@todo Processing: shutdown interaction - could be if shutdownInProcess then retry system deregistration once more
        } else {
            // System deregistration was successful
            // Processing: stop system deregistration
            confssProcessing.removeSystemToDeregister(systemURN);
        }
    }
    
    public void replyServiceSet(Result result, SystemURN systemURN, Set<ServiceURN> serviceURNs) {
    	logger.info(String.format("%s %s has services %s", result, systemURN, serviceURNs));
    }
    
    
    /**
     * @see IConfigurationClient#informDisconnect
     */
    public void informDisconnect() {
        // TODO Retry connection to CfgMngr if not shutting down */
    }

    /* ************************************************************************
     * Begin protected API 
     * ************************************************************************/
    
    /**
     * Special method to only be used by CfgMngr which will pass in null as the
     * ConfigurationServer address to the constructor and will call this prior to
     * calling duringInitialize().
     * @param locationURI the address for connecting to the configuration server
     */
    protected void setCfgMngrLocationURI(URI locationURI) {
        cfgmngrLocationURI = locationURI;
    }
    
    /**
     * Get the location of the configuration manager
     * @return the location of the config manager
     */
    protected URI getCfgMngrLocation() {
    	return cfgmngrLocationURI;
    }
    
    /**
     * @return handle to the configuration server
     */
    protected IConfigurationServer getConfigurationServer() {
    	return configurationServer;
    }
      
    /**
     * Pass the locate service request onto CfgMngr
     */
    protected void requestLocateService(ServiceURN serviceURN) {
        if (configurationServer == null) {
            logger.error("No connection to cfgmngr");
        } else {
            configurationServer.requestLocateService(this, serviceURN, "tcp");
        }
    }
    
    /**
     * Get system name from systemURN
     */
    protected String getSystemName() {
        return getSystemURN().getSystemName();
    }
    
    protected IConfigurationServer createConfigurationServer() {
    	return new ConfigurationServerProxy(cfgmngrLocationURI);
    }
    
    @Override
    protected void addSensors() throws URISyntaxException {
        super.addSensors();
        for (Sensor sensor : cpuUsage.getSensors()) {
            addSensor(getSystemURN(), sensor);
        }
    }
    
    /* ************************************************************************
     * GenSS overridden methods
     * ************************************************************************/

    /**
     * Initializing involves requesting configuration from CfgMngr and then
     * waiting for reply.
     */
    @Override
    protected void duringInitialize() throws Exception {
    	super.duringInitialize();
        configurationServer = createConfigurationServer();
        
        //Processing: Start timer to request configuration periodically
        confssProcessing.initialize(configurationServer, getTimer(), confssStatic.registrationCycle_sec * 1000);
        confssProcessing.addConfigurationRequest(getSystemURN());

        //Do not move into MANAGE mode, must first wait for configuration from CfgMngr
    }
    
    /**
     * Shutdown involves deregistering system from CfgMngr and then waiting
     * for reply.
     */
    @Override
    protected void duringShutdown() throws Exception {
        //No need to deregister any services as genss should trigger deregistration when it stops providing them
        //and CfgMngr will remove services attached to a system when it deregisters the system
        //Processing: Deregister subsystem 
        confssProcessing.addSystemToDeregister(getSystemURN());
        // Now give the deregister message some time to send before shutting down
        // processing (which prevents retries) and ultimately the informDisconnect
        // which closes the connection.
        Thread.sleep(20);
        
        confssProcessing.shutdownProcessing();
        // now stop the cpu usage monitoring
        if (cpuUsage != null) {
        	cpuUsage.shutdown();
        }

        configurationServer.informDisconnect(this);
        setMode(ModeEnum.SHUTDOWN_COMPLETE);
        super.duringShutdown();
    } 

    /* ************************************************************************
     * Registration/Deregistration callback methods for genss
     * ************************************************************************/
   
    @Override
    protected void deregisterService(ServiceURN serviceURN, URI serviceLocationURI) {
        // Processing: deregistration of service required 
    	Pair<ServiceURN, URI> pair = new Pair<ServiceURN, URI>(serviceURN, serviceLocationURI);
    	services.remove(pair);
        confssProcessing.addServiceToDeregister(pair);
    }

    @Override
    protected void registerService(ServiceURN serviceURN, URI serviceLocationURI) {
       // Processing: registration of service required
    	Pair<ServiceURN, URI> pair = new Pair<ServiceURN, URI>(serviceURN, serviceLocationURI);
    	services.add(pair);
        confssProcessing.addServiceToRegister(pair);
    }
    
    /* ************************************************************************
     * Private methods for internal use
     * ************************************************************************/

    /**
     * Loads the subsystem's property file. The file is named
     * subsystemStatic.properties It also stores the command line parameters in
     * confssStatic
     * Derived classes can extend ConfssStaticConfiguration and override this method if more static configuration is required
     * 
     * @throws FileNotFoundException
     *             if the property file could not be found
     * @throws IOException
     *             if file access fails
     * @throws URISyntaxException
     *             if the entry in the properties file is not a valid URI
     */
    private void loadStaticConf() { 
        String propertyFileName = getSystemName() + STATIC_PROPERTIES_FILE_PFX;
        logger.trace("Load static configuration: " + propertyFileName);
        try {
            // Load the properties file into confssStatic
            Properties properties = PropertyLoader.loadProperties(propertyFileName);
            confssStatic.loadProperties(properties);
        } catch (Exception ex) {
            logger.error(String.format("Cannot load static properties file %s for %s", propertyFileName, getSystemName()));
        }
    }

    /**
     * Loads the subsystem's dynamic configuration from the properties received
     * from CfgMngr The file is names subsystemStatic.properties
     * @param conf - Object containing properties received from CfgMngr
     * @param confDynamic - Confss Dynamic Configuration into which the properties must be loaded
     */
    private boolean loadDynamicConf(Object conf) {
        try {
            confssDynamic.loadProperties((Properties) conf);
            return (true);
        } catch (Exception ex) {
            return (false);
        }
    }
    
    /* ************************************************************************
     * Main      
     * ************************************************************************/

	/**
	 * Instantiates a ConfSS derived system using the command line parameters
	 * 
	 * @param args
	 *            the command line arguments
	 * @param args
	 *            [0] systemClass
	 * @param args
	 *            [1] systemURN
	 * @param args
	 *            [2] cfgmngrLocationURI
	 */
	public static void main(String args[]) {
		String systemClass = args[0];
		try {
			SystemURN systemURN = new SystemURN(args[1]);
			URI cfgmngrLocationURI = new URI(args[2]);
			Class<?> clazz = Class.forName(systemClass);
			Class<?>[] parameterTypes = { systemURN.getClass(), cfgmngrLocationURI.getClass() };
			Constructor<?> ct = clazz.getConstructor(parameterTypes);
			Object[] initargs = { systemURN, cfgmngrLocationURI };
			boolean restart = true;
			while (restart) {
				ConfSS system = (ConfSS) ct.newInstance(initargs);
				system.initialize();
				restart = system.waitHalt();
				system.shutdown();
			}
		} catch (URISyntaxException ex) {
			logger.fatal(String.format("Unable to start %s with invalid URI", systemClass), ex);
			System.exit(1);
		} catch (Exception ex) {
			logger.fatal(String.format("Error launching %s", systemClass), ex);
			System.exit(2);
		}
		System.exit(0);
	}

}
